package com.hz.tgb.data.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.TimeoutUtils;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * Cache工具类
 *
 * @author hezhao on 2019.11.13
 */
@Component
@Scope("prototype")
public class CacheOperations {

    public static long TIME_OUT = 7 * 24 * 60 * 60;

    @Autowired
    private RedisTemplate<String, Object> cacheTemplate;

    private int database = -1;

    public void setDatabase(int database) {
        this.database = database;
    }

    public <T> T get(String key, Class<T> theClass) {
        return (T) get(key);
    }

    public Object get(String key) {
        final byte[] rawKey = rawKey(key);
        byte[] rawValues = (byte[]) getCacheTemplate().execute((RedisCallback) connection -> {
            initRedisConnection(connection);
            return connection.get(rawKey);
        });
        return deserializeValue(rawValues);
    }

    public void set(String key, Object data) {
        set(key, data, TIME_OUT, TimeUnit.SECONDS);
    }

    public void set(final String key, Object data, final long timeout) {
        set(key, data, timeout, TimeUnit.SECONDS);
    }

    public void set(final String key, Object data, final long timeout, final TimeUnit unit) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawValue = rawValue(data);
        getCacheTemplate().execute(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                initRedisConnection(connection);
                potentiallyUsePsetEx(connection);
                return null;
            }

            public void potentiallyUsePsetEx(RedisConnection connection) {
                if (!TimeUnit.MILLISECONDS.equals(unit) || !failsafeInvokePsetEx(connection)) {
                    connection.setEx(rawKey, TimeoutUtils.toSeconds(timeout, unit), rawValue);
                }
            }

            private boolean failsafeInvokePsetEx(RedisConnection connection) {
                boolean failed = false;
                try {
                    connection.pSetEx(rawKey, timeout, rawValue);
                } catch (UnsupportedOperationException e) {
                    failed = true;
                }
                return !failed;
            }
        });
    }

    public Boolean expire(final String key, final long timeout, final TimeUnit unit) {
        final byte[] rawKey = rawKey(key);
        final long rawTimeout = TimeoutUtils.toMillis(timeout, unit);
        return execute(connection -> {
            try {
                initRedisConnection(connection);
                return connection.pExpire(rawKey, rawTimeout);
            } catch (Exception e) {
                return connection.expire(rawKey, TimeoutUtils.toSeconds(timeout, unit));
            }
        });
    }

    public boolean setBit(String key, long offset, boolean value) {
        return setBit(key, offset, value,TIME_OUT);
    }

    public boolean setBit(String key, long offset) {
        return setBit(key, offset, true);
    }

    public Boolean setBit(String key, long offset, boolean value,long timeout) {
        final byte[] rawKey = rawKey(key);
        cacheTemplate.expire(key, timeout, TimeUnit.SECONDS);
        Boolean result = execute(connection -> {
            initRedisConnection(connection);
            Boolean result1 = connection.setBit(rawKey, offset, value);
            connection.expire(rawKey, timeout);
            return result1;
        });
        return result;
    }

    public boolean hasKey(String key){
        final byte[] rawKey = rawKey(key);
        return execute(connection -> {
            initRedisConnection(connection);
            return connection.exists(rawKey);
        });
    }

    public void delKeys(List<String> keys){
        if (CollectionUtils.isEmpty(keys)) {
            return;
        }
        final byte[][] rawKeys = rawKeys(keys);
        execute(connection -> {
            initRedisConnection(connection);
            connection.del(rawKeys);
            return null;
        });
    }

    public void delKey(String key){
        final byte[] rawKey = rawKey(key);
        execute(connection -> {
            initRedisConnection(connection);
            connection.del(rawKey);
            return null;
        });
    }

    public boolean getBit(String key, final long offset) {
        final byte[] rawKey = rawKey(key);
        return execute(connection -> {
            initRedisConnection(connection);
            return connection.getBit(rawKey, offset);
        });
    }

    public List multiGet(List<String> keys) {
        if (keys.isEmpty()) {
            return Collections.emptyList();
        }

        final byte[][] rawKeys = new byte[keys.size()][];

        int counter = 0;
        for (String hashKey : keys) {
            rawKeys[counter++] = rawKey(hashKey);
        }

        List<byte[]> rawValues = execute(connection -> {
            initRedisConnection(connection);
            return connection.mGet(rawKeys);
        });
        return deserializeValues(rawValues);
    }

    public void multiSet(Map<String, Object> dataMap) {
        if (dataMap.isEmpty()) {
            return;
        }
        final Map<byte[], byte[]> rawKeys = new LinkedHashMap<byte[], byte[]>(dataMap.size());
        for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
            rawKeys.put(rawKey(entry.getKey()), rawValue(entry.getValue()));
        }
        execute(connection -> {
            initRedisConnection(connection);
            connection.mSet(rawKeys);
            return null;
        });
    }

    public void delete(final String... keys) {
        List<String> keyList = new ArrayList<>();
        for (String key : keys) {
            if (key != null) {
                keyList.add(key);
            }
        }
        if (CollectionUtils.isEmpty(keyList)) {
            return;
        }
        final byte[][] rawKeys = rawKeys(keyList);
        execute(connection -> {
            initRedisConnection(connection);
            connection.del(rawKeys);
            return null;
        });
    }

    public Long increment(String key) {
        return increment(key, 1);
    }

    public Long increment(String key, final long delta) {
        final byte[] rawKey = rawKey(key);
        return execute(connection -> {
            initRedisConnection(connection);
            return connection.incrBy(rawKey, delta);
        });
    }

    public Long decrement(String key) {
        return increment(key, -1);
    }

    public Long decrement(String key, long delta) {
        return increment(key, -delta);
    }


    public Object hget(String key, String hashKey) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(hashKey);
        byte[] rawHashValue = execute(connection -> {
            initRedisConnection(connection);
            return connection.hGet(rawKey, rawHashKey);
        });
        return (Object) deserializeHashValue(rawHashValue);
    }

    public void hset(String key, String hashKey, Object data) {
        hset(key, hashKey, data, TIME_OUT);
    }

    public void hset(String key, String hashKey, Object value, long timeout) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawHashKey = rawHashKey(hashKey);
        final byte[] rawHashValue = rawHashValue(value);
        execute(connection -> {
            initRedisConnection(connection);
            connection.hSet(rawKey, rawHashKey, rawHashValue);
            connection.expire(rawKey, timeout);
            return null;
        });

    }

    public void hsets(String key, Map<String, Object> data) {
        hsets(key, data, TIME_OUT);
    }

    public void hsets(String key, Map<String, Object> data, long timeout) {
        if (data.isEmpty()) {
            return;
        }
        final byte[] rawKey = rawKey(key);
        final Map<byte[], byte[]> hashes = new LinkedHashMap<byte[], byte[]>(data.size());
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            hashes.put(rawHashKey(entry.getKey()), rawHashValue(entry.getValue()));
        }
        execute(connection -> {
            initRedisConnection(connection);
            connection.hMSet(rawKey, hashes);
            connection.expire(rawKey, timeout);
            return null;
        });
    }

    public Long hdelete(String key, String... hashKeys) {
        final byte[] rawKey = rawKey(key);
        final byte[][] rawHashKeys = rawHashKeys(hashKeys);
        return execute(connection -> {
            initRedisConnection(connection);
            return connection.hDel(rawKey, rawHashKeys);
        });
    }

    public boolean setIfAbsent(String key, Object value, long timeout) {
        final byte[] rawKey = rawKey(key);
        final byte[] rawValue = rawValue(value);
        return execute(connection -> {
            initRedisConnection(connection);
            Boolean result = connection.setNX(rawKey, rawValue);
            if(result) {
                connection.expire(rawKey, timeout);
            }
            return result;
        });
    }

    private void initRedisConnection(RedisConnection connection) {
        if(database != -1) connection.select(database);
    }

    private byte[][] rawHashKeys(String ... hashKeys) {
        final byte[][] rawHashKeys = new byte[hashKeys.length][];
        int i = 0;
        for (String hashKey : hashKeys) {
            rawHashKeys[i++] = rawHashKey(hashKey);
        }
        return rawHashKeys;
    }

    private byte[] rawKey(String key) {
        return ((RedisSerializer<String>)cacheTemplate.getKeySerializer()).serialize(key);
    }

    private byte[][] rawKeys(Collection<String> keys) {
        final byte[][] rawKeys = new byte[keys.size()][];
        int i = 0;
        for (String key : keys) {
            rawKeys[i++] = rawKey(key);
        }
        return rawKeys;
    }

    private byte[] rawValue(Object value) {
        return ((RedisSerializer<Object>)cacheTemplate.getValueSerializer()).serialize(value);
    }

    private <T> T execute(RedisCallback<T> action) {
        return (T) getCacheTemplate().execute(action, getCacheTemplate().isExposeConnection());
    }

    private Object deserializeValue(byte[] rawValues) {
        return ((RedisSerializer<Object>)cacheTemplate.getValueSerializer()).deserialize(rawValues);
    }

    private List<Object> deserializeValues(List<byte[]> rawValues) {
        return SerializationUtils.deserialize(rawValues, valueSerializer());
    }

    private byte[] rawHashValue(Object value) {
        return ((RedisSerializer<Object>)cacheTemplate.getHashValueSerializer()).serialize(value);
    }

    private byte[] rawHashKey(String key) {
        return ((RedisSerializer<String>)cacheTemplate.getKeySerializer()).serialize(key);
    }

    private Object deserializeHashValue(byte[] value) {
        return (Object) getCacheTemplate().getHashValueSerializer().deserialize(value);
    }

    private RedisSerializer keySerializer() {
        return getCacheTemplate().getKeySerializer();
    }

    private RedisSerializer valueSerializer() {
        return getCacheTemplate().getValueSerializer();
    }

    public RedisTemplate getCacheTemplate() {
        return cacheTemplate;
    }

}
